Skip to content

v1.1.1: reboot self-heal, caddy loop, secret persistence, root guard, qbit RAM#2

Merged
LavX merged 4 commits into
mainfrom
fix/v1.1.1-reboot-caddy-secrets-root
Jul 6, 2026
Merged

v1.1.1: reboot self-heal, caddy loop, secret persistence, root guard, qbit RAM#2
LavX merged 4 commits into
mainfrom
fix/v1.1.1-reboot-caddy-secrets-root

Conversation

@LavX

@LavX LavX commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Fixes the v1.1.0 issues surfaced on Discord (Loki/Ryder), found via a multi-agent review of the reports.

Fixes

#1 Reboot ordering, qBittorrent loses gluetun's port after a restart (high)

depends_on is only honored on docker compose up, NOT on host/daemon reboot. A VPN-routed qbit (network_mode: service:gluetun) can start before gluetun and end up orphaned from its netns with the WebUI port dead; restart: unless-stopped can't help because qbit is up, just broken. Added a deunhealth sidecar (ro docker.sock) that restarts qbit when its healthcheck goes unhealthy. Only qbit carries the restart label, so nothing else is ever auto-restarted. arrstack update injects the sidecar into existing VPN installs, so current users get the fix without a fresh install.

#4 Per-service healthchecks (medium)

The catalog health: blocks were dead code (only gluetun got a healthcheck). They now render as container healthchecks using a portable curl || wget probe (verified against a live stack: some images ship only one). tcp/port: 0 blocks (caddy, recyclarr) are skipped to avoid false-unhealthy. depends_on is intentionally left short-form to avoid first-install deadlocks.

#2 Secret persistence on reconfigure/resume (high)

buildStateFromWizard regenerated api keys on every run, and the Bazarr/translator AES key + flask secret were minted fresh each install. Now persisted api keys and a new secrets block are reused. Also: arrstack install now stops with an actionable message if admin.txt exists but is unreadable (root-owned from a prior sudo run) instead of silently rotating the password and locking the user out of the running qbit.

#3 Caddy crash-loop in LAN-only + no-DNS (medium)

That combo rendered a 0-byte Caddyfile, so caddy run exited (adapting config: EOF) and looped under restart: unless-stopped. Now emits a minimal :80 site.

#5 Root-user guard (medium)

Preflight + wizard warnings when running as root, and PUID/PGID are clamped away from 0 (LinuxServer.io images break as root).

#6 qBittorrent RAM (medium)

enable_os_cache: false bounds the libtorrent-2.0 memory-mapped disk cache that can grow to many GB and OOM a small VM.

#7 Tooling

Added typescript to devDependencies (bun run typecheck previously failed with tsc: command not found).

Deliberately out of scope

  • No compose mem_limit for qbit (OOM-kills it if set too low; enable_os_cache fixes the cause).
  • No broad depends_on: service_healthy flip (risks first-install deadlocks).

Verification

  • bun test: 296 pass / 0 fail (+26 new tests)
  • bun run typecheck: clean

…ard, qbit RAM

Addresses the v1.1.0 issues reported on Discord (Loki/Ryder):

- Reboot ordering: add a deunhealth sidecar that restarts qBittorrent when its
  healthcheck fails. depends_on ordering is ignored on host reboot, so a
  VPN-routed qbit can start before gluetun and lose its published WebUI port,
  and restart:unless-stopped can't fix it because qbit is up, just orphaned.
  Only qbit is labeled, so nothing else is ever auto-restarted. arrstack update
  injects the sidecar into existing VPN installs too.
- Healthchecks: render the catalog health blocks (previously dead code) as
  container healthchecks using a portable curl-or-wget probe (arr/media images
  ship one or the other). Skip tcp/port-0 blocks (caddy, recyclarr).
- Caddy crash-loop: emit a minimal :80 site in LAN-only mode with no local DNS
  instead of a 0-byte Caddyfile that makes caddy exit and restart-loop.
- Secret persistence: reuse persisted api keys and a new secrets block (the
  Bazarr/translator AES key + Bazarr flask secret) across reconfigure/resume
  instead of rotating them out from under running containers. Stop the install
  with a fix message if admin.txt exists but is unreadable (root-owned) rather
  than silently generating a new password and locking the user out of qbit.
- Root guard: warn in preflight and the wizard status strip, and clamp
  PUID/PGID away from 0 so LinuxServer.io containers do not run as root.
- qBittorrent RAM: set enable_os_cache=false to bound the libtorrent-2.0
  memory-mapped disk cache that can grow to many GB and OOM a small VM.
- Tooling: add typescript to devDependencies so `bun run typecheck` works.

296 tests pass, typecheck clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 707e08e8da

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/renderer/compose.ts
Comment on lines +223 to +225
const url = `http://127.0.0.1:${h.port}${h.path}`;
return {
test: `["CMD-SHELL", "curl -fsS -o /dev/null ${url} || wget -q -O /dev/null ${url} || exit 1"]`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Probe qBittorrent through gluetun for self-heal

For VPN-routed qBittorrent, this healthcheck runs inside the qBittorrent container and curls its own loopback address. In the reboot/orphaned-netns case described below, qBittorrent can still serve 127.0.0.1:8080 inside its stale namespace while gluetun no longer publishes that port to the host, so Docker keeps the container healthy and deunhealth never restarts it. The probe needs to exercise the gluetun/published path (or otherwise detect the stale namespace) for the new sidecar to fix the reboot failure.

Useful? React with 👍 / 👎.

Comment thread src/platform/preflight.ts
Comment on lines +72 to +73
// 0. Not running as root (warning only, non-blocking)
results.push(checkNotRoot());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve doctor success for root warning

Adding this non-blocking warning to every preflight result makes arrstack doctor fail whenever it is run as root: the doctor loop marks allOk = false for any !check.ok result without checking blocking (src/usecase/doctor.ts:69-76). That turns the new advisory into a failing diagnostic exit even though checkNotRoot() explicitly sets blocking: false; either doctor should treat non-blocking checks as warnings or this check should be skipped there.

Useful? React with 👍 / 👎.

Comment thread src/cli.ts Outdated
// non-sudo run then can't read it, would generate a NEW admin password,
// rewrite qBittorrent.conf, and lock the user out of the still-running qbit
// container. Stop early with a fix instead of silently rotating.
if (!opts.fresh && adminTxtUnreadable(installDir)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the admin.txt guard to the default TUI

This protection only runs for arrstack install, but the same TUI can still be launched through bare arrstack (src/cli.ts:143-146) without calling adminTxtUnreadable. In that entrypoint, an unreadable root-owned admin.txt is still swallowed by readExistingAdminPassword, a new admin password is generated, and the qBittorrent lockout scenario described here can still happen when users reconfigure via the default command.

Useful? React with 👍 / 👎.

Comment thread src/usecase/install.ts Outdated
Comment on lines +179 to +180
const translatorEncryptionKey =
secrets.translator_encryption_key ?? randomBytes(32).toString("hex");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Backfill legacy secrets before generating new ones

For an install whose state.json was written before the new secrets field, the schema defaults state.secrets to {}, so the first reconfigure after upgrading still takes this fallback and mints a new translator ENCRYPTION_KEY (and Flask secret). The old key is already present in .env/Bazarr config but is ignored here, so this still orphans encrypted translator data for existing users; migrate from the existing files before falling back to random generation.

Useful? React with 👍 / 👎.

- P1: VPN-routed qBittorrent healthcheck now probes tunnel connectivity
  (cp.cloudflare.com/generate_204) instead of loopback 8080. A loopback probe
  passes even when qbit is orphaned from gluetun's netns after a reboot, so
  deunhealth would never restart it; the tunnel probe only succeeds in gluetun's
  live netns, which is exactly when the published WebUI port also routes.
- P2: arrstack doctor no longer fails on a non-blocking preflight check. The
  root-user warning (blocking: false) prints as a warning, not a failing exit.
- P2: apply the unreadable-admin.txt guard to bare `arrstack` (the default TUI
  entrypoint) too, not just `arrstack install`, via a shared helper.
- P2: on upgrade, backfill the translator ENCRYPTION_KEY and Bazarr flask secret
  from the existing .env / Bazarr config before generating new ones, so the
  first reconfigure after upgrading doesn't rotate keys and orphan data
  encrypted at rest.

301 tests pass, typecheck clean.
@LavX

LavX commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

Addressed all four Codex findings in 05e7552:

  • P1 (qbit self-heal probe): VPN-routed qBittorrent now probes tunnel connectivity (cp.cloudflare.com/generate_204, curl-or-wget, verified through gluetun) instead of loopback 8080, so deunhealth actually detects the orphaned-netns state after a reboot.
  • P2 (doctor): non-blocking preflight checks now print as warnings; the root-user advisory no longer flips arrstack doctor to a failing exit.
  • P2 (admin.txt guard): extracted to a shared helper and applied to bare arrstack as well as arrstack install.
  • P2 (legacy secrets): on upgrade, the translator ENCRYPTION_KEY and Bazarr flask secret are recovered from the existing .env / Bazarr config before falling back to generation, so the first reconfigure after upgrading doesn't rotate them.

301 tests pass, typecheck clean.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 05e755286b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…secret regex, tests)

A multi-agent review of the v1.1.1 branch surfaced these (all low/medium, no criticals):

- VPN qbit healthcheck now probes gluetun's control server (127.0.0.1:8000, via
  the shared netns) instead of a third-party host. The old external probe could
  not tell an orphaned netns from a real tunnel/WAN outage, so deunhealth would
  restart-loop qbit during a genuine outage. The control server is up whenever
  the gluetun container is up, independent of the tunnel, so a real outage no
  longer churns qbit; -f is omitted so an auth 401 still counts as reachable.
  Verified reachable from the running qbit container.
- arrstack update now persists deunhealth into state.services_enabled, not just
  the rendered compose, so doctor can verify the sidecar and later updates don't
  re-add/re-log it (state/compose drift).
- Harden the Bazarr flask_secret migration regex: anchored, same-line value,
  strips optional quotes, so a re-serialized quoted/empty/commented key falls
  through to generation instead of capturing the wrong text.
- Tests: cover the doctor warn-vs-fail path (classifyCheck), the admin.txt guard
  message (adminTxtGuardMessage), flask regex edge cases, and the update
  no-re-add path; reframe the tautological restart test into a real
  reboot-recovery-trio assertion.

310 tests pass, typecheck clean.
@LavX

LavX commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

Addressed a follow-up multi-agent code review in 8822c64 (all findings were low/medium, no criticals):

  • VPN self-heal probe: switched qbit's healthcheck from a third-party host to gluetun's control server (127.0.0.1:8000, reachable via the shared netns, verified live). It detects the orphaned-netns state but stays healthy during a real tunnel/WAN outage, so deunhealth no longer restart-loops qbit when the VPN is genuinely down.
  • update state drift: arrstack update now persists deunhealth into state.services_enabled, so doctor verifies the sidecar and later updates don't re-add/re-log it.
  • flask_secret migration regex: anchored + strips quotes so a re-serialized quoted/empty/commented value falls through to generation instead of being captured wrong.
  • tests: added coverage for the doctor warn path, the admin.txt guard message, flask regex edge cases, and the update no-re-add path; reframed a tautological test.

310 tests pass, typecheck clean.

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8822c64aad

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/renderer/compose.ts Outdated
const GLUETUN_CONTROL_URL = "http://127.0.0.1:8000/v1/publicip/ip";
function buildVpnHealthcheck(): Healthcheck {
return {
test: `["CMD-SHELL", "curl -sS -m 10 -o /dev/null ${GLUETUN_CONTROL_URL} || exit 1"]`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include qBittorrent itself in the VPN healthcheck

When VPN routing is enabled this healthcheck is the signal that arms deunhealth, but it only verifies that gluetun's control server is reachable. If qBittorrent's WebUI/process is hung or no longer listening while gluetun is up, Docker will still report the qBittorrent container as healthy and deunhealth will never restart it; the VPN-specific probe should also check qBittorrent's local WebUI (in addition to the gluetun netns probe) so actual qbit failures are still self-healed.

Useful? React with 👍 / 👎.

The control-server-only probe proves qbit is in gluetun's live netns (catches
the reboot orphaned-netns case) but would stay healthy if qbit's WebUI process
hangs while gluetun is up, so deunhealth would never restart a hung qbit. Require
BOTH legs: gluetun control server reachable AND qbit's WebUI responding. Verified
both legs pass on a live VPN-routed qbit container.

310 tests pass, typecheck clean.
@LavX

LavX commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

Addressed the re-review P2 in the latest commit: the VPN qbit healthcheck now requires BOTH legs, gluetun's control server reachable AND qbit's own WebUI responding, so a hung qbit (process up, UI dead) while gluetun is fine is now caught and self-healed too. Verified both legs pass on the live container. 310 tests pass, typecheck clean. @codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@LavX LavX merged commit 71d5685 into main Jul 6, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant